package com.yb.custom.annotation.log.annotation.handler;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.yb.custom.annotation.log.annotation.SignatureVerify;
import com.yb.custom.annotation.log.exception.CustomException;
import com.yb.custom.annotation.log.wrapper.MyRequestWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

/**
 * 接口签名注解处理类
 * 可以使用RSA的公私钥处理请求:(这样更安全,但是加密后的参数更长更大)
 * 1.请求客户端使用服务端提供的公钥加密请求参数信息(在原来的基础上)
 * 2.服务端先使用私钥解密参数,然后进行验证签名处理,这样既能保证数据不可见,还能验证是否被篡改
 *
 * @author yangbiao
 * @date 2022/3/30
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class SignatureVerifyHandler {
    @Value("${third.expireTime}")
    private long expireTime;
    @Value("${third.appSecret}")
    private String appSecret;
    private final StringRedisTemplate stringRedisTemplate;
    private static final String APP_ID = "appId";
    private static final String APP_SECRET = "appSecret";
    private static final String NONCE_PARAM = "nonce";
    private static final String TIMESTAMP_PARAM = "timestamp";
    private static final String SING_PARAM = "sign";
    private static final String REQUEST_GET = "GET";
    private static final String REQUEST_POST = "POST";

    /**
     * 切入点签名,切入点表达式,可以指定包范围,可以指定注解,还可以两个一起用
     */
    @Pointcut(value = "@annotation(com.yb.custom.annotation.log.annotation.SignatureVerify)")
    public void signatureVerifyPointcut() {
    }

    /**
     * 这里只用了环绕切的方式,如有需要,可另行添加其他的方式
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "signatureVerifyPointcut()")
    public Object logRecordAround(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            //获取HttpServletRequest请求对象
            HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes()).getRequest();

            //获取可重复使用请求
            final MyRequestWrapper request = new MyRequestWrapper(httpServletRequest);

            //把系统参数放在请求头里,可以方便处理请求参数,否则需要做很多处理
            //但是需要和参数一样,使用key做正序排序,使用TreeMap即可,然后进行加密生成签名
            final String sign = request.getHeader(SING_PARAM);
            final String appId = request.getHeader(APP_ID);
            final String nonce = request.getHeader(NONCE_PARAM);
            final String timestampStr = request.getHeader(TIMESTAMP_PARAM);
            //判断师傅缺少请求头信息
            if (StrUtil.isBlank(sign)) {
                CustomException.message("缺少请求头sign");
            }
            if (StrUtil.isBlank(appId)) {
                CustomException.message("缺少请求头appId");
            }
            if (StrUtil.isBlank(nonce)) {
                CustomException.message("缺少请求头nonce");
            }
            if (StrUtil.isBlank(timestampStr)) {
                CustomException.message("缺少请求头timestamp");
            }
            //解析时间戳
            final long timestamp = Long.parseLong(timestampStr);

            //头信息里的系统参数,不能和请求参数放在一起,避免被请求参数覆盖掉,
            //导致自己手里的秘钥被替换后,被验签通过(虽然在后面添加头信息可以处理,但是还是分开比较好)
            Map<String, Object> headerTreeMap = new TreeMap<>();
            //添加到参数treeMap集合
            headerTreeMap.put(APP_ID, appId);
            headerTreeMap.put(APP_SECRET, appSecret);
            headerTreeMap.put(NONCE_PARAM, nonce);
            headerTreeMap.put(TIMESTAMP_PARAM, timestampStr);

            //获取请求路径
            final String servletPath = request.getServletPath();
            //生成随机数存储到redis上的key
            String nonceRedisKey = NONCE_PARAM.concat("@").concat(nonce).concat("@")
                    .concat(appId).concat("@").concat(servletPath);

            //创建一个有序的Map集合(默认key正序排序)
            Map<String, Object> treeMap = new TreeMap<>();

            //GET请求方式
            if (REQUEST_GET.equalsIgnoreCase(request.getMethod())) {
                final Enumeration<String> names = request.getParameterNames();
                while (names.hasMoreElements()) {
                    final String paramKey = names.nextElement();
                    if (!SING_PARAM.equals(paramKey)) {
                        treeMap.put(paramKey, request.getParameter(paramKey));
                    }
                }
            }

            //POST请求方式
            if (REQUEST_POST.equalsIgnoreCase(request.getMethod())) {
                //判断请求数据是否是json形式
                final String contentType = request.getContentType();
                if (!contentType.equals(MediaType.APPLICATION_JSON_VALUE)) {
                    CustomException.message("请使用JSON格式的参数");
                }
                //获取请求体参数
                final String body = request.getBody();
                //解析为TreeMap
                treeMap = JSONObject.parseObject(body, TreeMap.class);
            }

            //验证时间戳是否已经超时
            final long lastExpireTime = timestamp + expireTime * 1000;
            final long currentTime = System.currentTimeMillis();
            if (currentTime > lastExpireTime) {
                log.error("验签失败,请求已过期");
                CustomException.message("验签失败");
            }

            //验证是否是重复请求
            final Boolean hasKey = stringRedisTemplate.hasKey(nonceRedisKey);
            if (hasKey) {
                log.error("验签失败,重复的请求");
                CustomException.message("验签失败");
            }
            //添加随机数到redis里(过期时间和上面的时间戳过期时间一致,因为超过了就无法执行到这儿,存着也没有用了)
            stringRedisTemplate.opsForValue().set(nonceRedisKey, nonce, expireTime, TimeUnit.SECONDS);

            //把map中的参数处理成get请求的参数拼接的形式
            final String paramUrl = HttpUtil.toParams(treeMap);
            final String headerParam = HttpUtil.toParams(headerTreeMap);
            //清空内存里的treeMap信息(避免信息(秘钥)泄露)
            treeMap.clear();
            //摘要算法使用SHA256替代MD5(抗碰撞能力弱,容易被破解),
            //hash碰撞(就是不同的值处理后生成相同的加密字符串)
            final String newSign = DigestUtil.sha256Hex(paramUrl.concat("&").concat(headerParam));
            //比对签名是否一致
            if (!newSign.equals(sign)) {
                log.error("验签失败,签名不一致");
                CustomException.message("验签失败");
            }
        } catch (Throwable e) {
            log.error("接口验证签名异常: {}", e.getMessage(), e);
            if (e instanceof CustomException) {
                CustomException.message(e.getMessage());
            }
            CustomException.message("验签失败");
        }
        //继续执行下一个通知或目标方法调用,参数是Object[]类型的
        return joinPoint.proceed();
    }

}